/* * Copyright (C) 2014 IUH �yber$oft Team * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package vn.cybersoft.obs.android.provider; import java.text.DateFormatSymbols; import java.util.Calendar; import vn.cybersoft.obs.android.R; import vn.cybersoft.obs.android.utilities.Log; import vn.cybersoft.obs.android.utilities.Utils; import android.annotation.SuppressLint; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.Settings; import android.support.v4.content.CursorLoader; import android.text.format.DateFormat; /** * @author Atom * */ public class TimeSchedule implements Parcelable, DataProviderApi.TimeSchedulesColumns { // This action triggers the ScheduleReceiver as well as the TimeScheduleExcutorService. It // is a public action used in the manifest for receiving TimeSchedule broadcasts // from the alarm manager. public static final String EXECUTE_SCHEDULE_ACTION = "vn.cybersoft.obs.android.intent.action.EXECUTE_TIME_SCHEDULE"; // This extra is the raw TimeSchedule object data. It is used in the // AlarmManagerService to avoid a ClassNotFoundException when filling in // the Intent extras. public static final String INTENT_RAW_DATA = "intent.extra.schedule_raw"; // This string is used when passing an schedules object through an intent. public static final String INTENT_EXTRA = "intent.extra.schedule"; // This string is used to identify the schedule id passed to SetTimeSchedule from the // list of time schedules. public static final String EXTRA_ID = "time_schedule_id"; private final static String DM12 = "E h:mm aa"; private final static String DM24 = "E k:mm"; private final static String M12 = "h:mm aa"; public final static String M24 = "kk:mm"; /** * schedule start with an invalid id when it hasn't been saved to the database. */ public static final long INVALID_ID = -1; // Used when filtering enabled schedules. public static final String WHERE_ENABLED = ENABLED + "=1"; /** * The default sort order for this table */ public static final String DEFAULT_SORT_ORDER = HOUR + ", " + MINUTES + " ASC"; public static final String[] QUERY_COLUMNS = { _ID, HOUR, MINUTES, DAYS_OF_WEEK, SCHEDULE_TIME, ENABLED, MODE_ID }; /** * These save calls to cursor.getColumnIndexOrThrow() * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS */ public static final int ID_INDEX = 0; public static final int HOUR_INDEX = 1; public static final int MINUTES_INDEX = 2; public static final int DAYS_OF_WEEK_INDEX = 3; public static final int SCHEDULE_TIME_INDEX = 4; public static final int ENABLED_INDEX = 5; public static final int MODE_ID_INDEX = 6; private static final int COLUMN_COUNT = MODE_ID_INDEX + 1; private static ContentValues createContentValues(TimeSchedule schedule) { ContentValues values = new ContentValues(COLUMN_COUNT); // Set the schedule_time value if this schedule does not repeat. This will be // used later to disable expire schedules. @SuppressWarnings("unused") long time = 0; if (!schedule.daysOfWeek.isRepeatSet()) { time = calculateTimeSchedule(schedule); } values.put(ENABLED, schedule.enabled ? 1 : 0); values.put(HOUR, schedule.hour); values.put(MINUTES, schedule.minutes); values.put(SCHEDULE_TIME, schedule.time); values.put(DAYS_OF_WEEK, schedule.daysOfWeek.getCoded()); values.put(MODE_ID, schedule.modeId); return values; } public static Uri getUri(int scheduleId) { return ContentUris.withAppendedId(CONTENT_URI, scheduleId); } public static long getId(Uri contentUri) { return ContentUris.parseId(contentUri); } public static CursorLoader getSchedulesCursorLoader(Context context) { return new CursorLoader(context, CONTENT_URI, QUERY_COLUMNS, null, null, DEFAULT_SORT_ORDER); } /** * Creates a new time schedule and fills in the given schedule's id. */ public static long addTimeSchedule(Context context, TimeSchedule schedule) { ContentValues values = createContentValues(schedule); Uri uri = context.getContentResolver().insert( CONTENT_URI, values); schedule.id = (int) ContentUris.parseId(uri); setNextAction(context); return calculateTimeSchedule(schedule); } public static void deleteTimeSchedule(Context context, long scheduleId) { if (scheduleId == -1) return; ContentResolver contentResolver = context.getContentResolver(); Uri uri = ContentUris.withAppendedId(CONTENT_URI, scheduleId); contentResolver.delete(uri, "", null); setNextAction(context); } /** * Queries all time schedules * @return cursor over all schedules */ public static Cursor getTimeSchedulesCursor(ContentResolver contentResolver) { return contentResolver.query( CONTENT_URI, QUERY_COLUMNS, null, null, DEFAULT_SORT_ORDER); } // Private method to get a more limited set of schedules from the database. private static Cursor getFilteredTimeSchedulesCursor(ContentResolver contentResolver) { return contentResolver.query(CONTENT_URI, QUERY_COLUMNS, WHERE_ENABLED, null, null); } /** * Return an TimeSchedule object representing the schedule id in the database. * Returns null if no schedule exists. */ public static TimeSchedule getTimeSchedule(ContentResolver contentResolver, long scheduleId) { Cursor cursor = contentResolver.query( ContentUris.withAppendedId(CONTENT_URI, scheduleId), QUERY_COLUMNS, null, null, null); TimeSchedule schedule = null; if (cursor != null) { if (cursor.moveToFirst()) { schedule = new TimeSchedule(cursor); } cursor.close(); } return schedule; } /** * A convenience method to set an time schedule in the Time Schedule * content provider. * @return Time when the time schedule will fire. */ public static long setTimeSchedule(Context context, TimeSchedule timeSchedule) { ContentValues values = createContentValues(timeSchedule); ContentResolver resolver = context.getContentResolver(); resolver.update( ContentUris.withAppendedId(CONTENT_URI, timeSchedule.id), values, null, null); setNextAction(context); return calculateTimeSchedule(timeSchedule); } /** * A convenience method to enable or disable an time schedule. * * @param id corresponds to the _id column * @param enabled corresponds to the ENABLED column */ public static void enableTimeSchedule( final Context context, final long id, boolean enabled) { enableTimeScheduleInternal(context, id, enabled); setNextAction(context); } private static void enableTimeScheduleInternal(final Context context, final long id, boolean enabled) { enableTimeScheduleInternal(context, getTimeSchedule(context.getContentResolver(), id), enabled); } private static void enableTimeScheduleInternal(final Context context, final TimeSchedule schedule, boolean enabled) { if (schedule == null) { return; } ContentResolver resolver = context.getContentResolver(); ContentValues values = new ContentValues(2); values.put(ENABLED, enabled ? 1 : 0); // If we are enabling the schedule, calculate schedule time since the time // value in TimeSchedule may be old. if (enabled) { long time = 0; if (!schedule.daysOfWeek.isRepeatSet()) { time = calculateTimeSchedule(schedule); } values.put(SCHEDULE_TIME, time); } resolver.update(ContentUris.withAppendedId( CONTENT_URI, schedule.id), values, null, null); } public static TimeSchedule calculateNextAction(final Context context) { TimeSchedule schedule = null; long minTime = Long.MAX_VALUE; long now = System.currentTimeMillis(); Cursor cursor = getFilteredTimeSchedulesCursor(context.getContentResolver()); if (cursor != null) { if (cursor.moveToFirst()) { do { TimeSchedule s = new TimeSchedule(cursor); if (s.time == 0) { s.time = calculateTimeSchedule(s); } else if (s.time < now) { Log.v("Disabling expired schedule set for " + Log.formatTime(s.time)); // Expired schedule, disable it and move along. enableTimeScheduleInternal(context, s, false); continue; } if (s.time < minTime) { minTime = s.time; schedule = s; } } while (cursor.moveToNext()); } cursor.close(); } return schedule; } /** * Disables non-repeating schedules that have passed. Called at * boot. */ public static void disableExpiredSchedules(final Context context) { Cursor cur = getFilteredTimeSchedulesCursor(context.getContentResolver()); long now = System.currentTimeMillis(); if (cur.moveToFirst()) { do { TimeSchedule schedule = new TimeSchedule(cur); // A time of 0 means this schedule repeats. If the time is // non-zero, check if the time is before now. if (schedule.time != 0 && schedule.time < now) { Log.v("Disabling expired schedule set for " + Log.formatTime(schedule.time)); enableTimeScheduleInternal(context, schedule, false); } } while (cur.moveToNext()); } cur.close(); } /** * Called at system startup, on time/timezone change, and whenever the user * changes schedule settings */ public static void setNextAction(final Context context) { TimeSchedule schedule = calculateNextAction(context); if (schedule != null) { enableAction(context, schedule, schedule.time); } else { disableAction(context); } } /** * Sets action in AlarmManger. This is what will * actually launch the action when the schedule triggers. * * @param schedule TimeSchedule. * @param atTimeInMillis milliseconds since epoch */ @SuppressLint("NewApi") private static void enableAction(Context context, final TimeSchedule schedule, final long atTimeInMillis) { if (Log.LOGV) { Log.v("** setSchedule id " + schedule.id + " atTime " + atTimeInMillis); } Intent intent = new Intent(EXECUTE_SCHEDULE_ACTION); // XXX: This is a slight hack to avoid an exception in the remote // AlarmManagerService process. The AlarmManager adds extra data to // this Intent which causes it to inflate. Since the remote process // does not know about the TimeSchedule class, it throws a // ClassNotFoundException. // // To avoid this, we marshall the data ourselves and then parcel a plain // byte[] array. The ScheduleReceiver class knows to build the TimeSchedule // object from the byte[] array. Parcel out = Parcel.obtain(); schedule.writeToParcel(out, 0); out.setDataPosition(0); intent.putExtra(INTENT_RAW_DATA, out.marshall()); PendingIntent sender = PendingIntent.getBroadcast( context, schedule.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (Utils.isKitKatOrLater()) { am.setExact(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender); } else { am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender); } Calendar c = Calendar.getInstance(); c.setTimeInMillis(atTimeInMillis); String timeString = formatDayAndTime(context, c); saveNextAlarm(context, timeString); } /** * @param id Schedule ID. * */ static void disableAction(Context context) { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); PendingIntent sender = PendingIntent.getBroadcast( context, 0, new Intent(EXECUTE_SCHEDULE_ACTION), PendingIntent.FLAG_CANCEL_CURRENT); am.cancel(sender); saveNextAlarm(context, ""); } private static long calculateTimeSchedule(TimeSchedule schedule) { return calculateTimeSchedule(schedule.hour, schedule.minutes, schedule.daysOfWeek) .getTimeInMillis(); } static Calendar calculateTimeSchedule(int hour, int minute, TimeSchedule.DaysOfWeek daysOfWeek) { // start with now Calendar c = Calendar.getInstance(); c.setTimeInMillis(System.currentTimeMillis()); int nowHour = c.get(Calendar.HOUR_OF_DAY); int nowMinute = c.get(Calendar.MINUTE); // if schedule is behind current time, advance one day if (hour < nowHour || hour == nowHour && minute <= nowMinute) { c.add(Calendar.DAY_OF_YEAR, 1); } c.set(Calendar.HOUR_OF_DAY, hour); c.set(Calendar.MINUTE, minute); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); int addDays = daysOfWeek.getNextSchedule(c); if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays); return c; } public static String formatTime(final Context context, int hour, int minute, TimeSchedule.DaysOfWeek daysOfWeek) { Calendar c = calculateTimeSchedule(hour, minute, daysOfWeek); return formatTime(context, c); } public static String formatTime(final Context context, Calendar c) { String format = get24HourMode(context) ? M24 : M12; return (c == null) ? "" : (String) DateFormat.format(format, c); } private static String formatDayAndTime(final Context context, Calendar c) { String format = get24HourMode(context) ? DM24 : DM12; return (c == null) ? "" : (String)DateFormat.format(format, c); } static void saveNextAlarm(final Context context, String timeString) { android.provider.Settings.System.putString(context.getContentResolver(), Settings.System.NEXT_ALARM_FORMATTED, timeString); } /** * @return true if clock is set to 24-hour mode */ public static boolean get24HourMode(final Context context) { return android.text.format.DateFormat.is24HourFormat(context); } public static final Parcelable.Creator<TimeSchedule> CREATOR = new Parcelable.Creator<TimeSchedule>() { @Override public TimeSchedule createFromParcel(Parcel p) { return new TimeSchedule(p); } @Override public TimeSchedule[] newArray(int size) { return new TimeSchedule[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel p, int flags) { p.writeLong(id); p.writeInt(enabled ? 1 : 0); p.writeInt(hour); p.writeInt(minutes); p.writeInt(daysOfWeek.getCoded()); p.writeLong(time); p.writeLong(modeId); } public long id; public boolean enabled; public int hour; public int minutes; public DaysOfWeek daysOfWeek; public long time; public long modeId; public TimeSchedule(Cursor c) { id = c.getLong(ID_INDEX); enabled = c.getInt(ENABLED_INDEX) >= 1; hour = c.getInt(HOUR_INDEX); minutes = c.getInt(MINUTES_INDEX); daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX)); time = c.getLong(SCHEDULE_TIME_INDEX); modeId = c.getLong(MODE_ID_INDEX); } public TimeSchedule(Parcel p) { id = p.readLong(); enabled = p.readInt() == 1; hour = p.readInt(); minutes = p.readInt(); daysOfWeek = new DaysOfWeek(p.readInt()); time = p.readLong(); modeId = p.readInt(); } // Creates a default schedule at the current time. public TimeSchedule() { id = -1; Calendar c = Calendar.getInstance(); c.setTimeInMillis(System.currentTimeMillis()); hour = c.get(Calendar.HOUR_OF_DAY); minutes = c.get(Calendar.MINUTE); daysOfWeek = new DaysOfWeek(0x7f); modeId = -1; } @Override public int hashCode() { return Long.valueOf(id).hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof TimeSchedule)) return false; final TimeSchedule other = (TimeSchedule) o; return id == other.id; } @Override public String toString() { return "TimeSchedule{" + ", id=" + id + ", enabled=" + enabled + ", hour=" + hour + ", minutes=" + minutes + ", daysOfWeek=" + daysOfWeek + ", time=" + time + ", modeId=" + modeId + '}'; } /* * Days of week code as a single int. * 0x00: no day * 0x01: Monday * 0x02: Tuesday * 0x04: Wednesday * 0x08: Thursday * 0x10: Friday * 0x20: Saturday * 0x40: Sunday */ public static final class DaysOfWeek { private static int[] DAY_MAP = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY, Calendar.SUNDAY, }; // Bitmask of all repeating days private int mDays; public DaysOfWeek(int days) { mDays = days; } public String toString(Context context, boolean showNever) { StringBuilder ret = new StringBuilder(); // no days if (mDays == 0) { return showNever ? context.getText(R.string.never).toString() : ""; } // every day if (mDays == 0x7f) { return context.getText(R.string.every_day).toString(); } // count selected days int dayCount = 0, days = mDays; while (days > 0) { if ((days & 1) == 1) dayCount++; days >>= 1; } // short or long form? DateFormatSymbols dfs = new DateFormatSymbols(); String[] dayList = (dayCount > 1) ? dfs.getShortWeekdays() : dfs.getWeekdays(); // selected days for (int i = 0; i < 7; i++) { if ((mDays & (1 << i)) != 0) { ret.append(dayList[DAY_MAP[i]]); dayCount -= 1; if (dayCount > 0) ret.append( context.getText(R.string.day_concat)); } } return ret.toString(); } private boolean isSet(int day) { return ((mDays & (1 << day)) > 0); } public void set(int day, boolean set) { if (set) { mDays |= (1 << day); } else { mDays &= ~(1 << day); } } public void set(DaysOfWeek dow) { mDays = dow.mDays; } public int getCoded() { return mDays; } // Returns days of week encoded in an array of booleans. public boolean[] getBooleanArray() { boolean[] ret = new boolean[7]; for (int i = 0; i < 7; i++) { ret[i] = isSet(i); } return ret; } public boolean isRepeatSet() { return mDays != 0; } /** * returns number of days from today until next schedule * @param c must be set to today */ public int getNextSchedule(Calendar c) { if (mDays == 0) { return -1; } int today = (c.get(Calendar.DAY_OF_WEEK) + 5) % 7; int day = 0; int dayCount = 0; for (; dayCount < 7; dayCount++) { day = (today + dayCount) % 7; if (isSet(day)) { break; } } return dayCount; } } }